/*-------------------------------------------------------------------------
 *
 * rewriteheap.c
 *	  Support functions to rewrite tables.
 *
 * These functions provide a facility to completely rewrite a heap, while
 * preserving visibility information and update chains.
 *
 * INTERFACE
 *
 * The caller is responsible for creating the new heap, all catalog
 * changes, supplying the tuples to be written to the new heap, and
 * rebuilding indexes.  The caller must hold AccessExclusiveLock on the
 * target table, because we assume no one else is writing into it.
 *
 * To use the facility:
 *
 * begin_heap_rewrite
 * while (fetch next tuple)
 * {
 *	   if (tuple is dead)
 *		   rewrite_heap_dead_tuple
 *	   else
 *	   {
 *		   // 在此处执行任何必要的转换
 *		   rewrite_heap_tuple
 *	   }
 * }
 * end_heap_rewrite
 *
 * The contents of the new relation shouldn't be relied on until after
 * end_heap_rewrite is called.
 *
 *
 * IMPLEMENTATION
 *
 * This would be a fairly trivial affair, except that we need to maintain
 * the ctid chains that link versions of an updated tuple together.
 * Since the newly stored tuples will have tids different from the original
 * ones, if we just copied t_ctid fields to the new table the links would
 * be wrong.  When we are required to copy a (presumably recently-dead or
 * delete-in-progress) tuple whose ctid doesn't point to itself, we have
 * to substitute the correct ctid instead.
 *
 * For each ctid reference from A -> B, we might encounter either A first
 * or B first.  (Note that a tuple in the middle of a chain is both A and B
 * of different pairs.)
 *
 * If we encounter A first, we'll store the tuple in the unresolved_tups
 * hash table. When we later encounter B, we remove A from the hash table,
 * fix the ctid to point to the new location of B, and insert both A and B
 * to the new heap.
 *
 * If we encounter B first, we can insert B to the new heap right away.
 * We then add an entry to the old_new_tid_map hash table showing B's
 * original tid (in the old heap) and new tid (in the new heap).
 * When we later encounter A, we get the new location of B from the table,
 * and can write A immediately with the correct ctid.
 *
 * Entries in the hash tables can be removed as soon as the later tuple
 * is encountered.  That helps to keep the memory usage down.  At the end,
 * both tables are usually empty; we should have encountered both A and B
 * of each pair.  However, it's possible for A to be RECENTLY_DEAD and B
 * entirely DEAD according to HeapTupleSatisfiesVacuum, because the test
 * for deadness using OldestXmin is not exact.  In such a case we might
 * encounter B first, and skip it, and find A later.  Then A would be added
 * to unresolved_tups, and stay there until end of the rewrite.  Since
 * this case is very unusual, we don't worry about the memory usage.
 *
 * Using in-memory hash tables means that we use some memory for each live
 * update chain in the table, from the time we find one end of the
 * reference until we find the other end.  That shouldn't be a problem in
 * practice, but if you do something like an UPDATE without a where-clause
 * on a large table, and then run CLUSTER in the same transaction, you
 * could run out of memory.  It doesn't seem worthwhile to add support for
 * spill-to-disk, as there shouldn't be that many RECENTLY_DEAD tuples in a
 * table under normal circumstances.  Furthermore, in the typical scenario
 * of CLUSTERing on an unchanging key column, we'll see all the versions
 * of a given tuple together anyway, and so the peak memory usage is only
 * proportional to the number of RECENTLY_DEAD versions of a single row, not
 * in the whole table.  Note that if we do fail halfway through a CLUSTER,
 * the old table is still valid, so failure is not catastrophic.
 *
 * We can't use the normal heap_insert function to insert into the new
 * heap, because heap_insert overwrites the visibility information.
 * We use a special-purpose raw_heap_insert function instead, which
 * is optimized for bulk inserting a lot of tuples, knowing that we have
 * exclusive access to the heap.  raw_heap_insert builds new pages in
 * local storage.  When a page is full, or at the end of the process,
 * we insert it to WAL as a single record and then write it to disk
 * directly through smgr.  Note, however, that any data sent to the new
 * heap's TOAST table will go through the normal bufmgr.
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994-5, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/heap/rewriteheap.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <sys/stat.h>
#include <unistd.h>

#include "access/heapam.h"
#include "access/heapam_xlog.h"
#include "access/heaptoast.h"
#include "access/rewriteheap.h"
#include "access/transam.h"
#include "access/xact.h"
#include "access/xloginsert.h"
#include "catalog/catalog.h"
#include "lib/ilist.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "replication/logical.h"
#include "replication/slot.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
#include "storage/procarray.h"
#include "storage/smgr.h"
#include "utils/memutils.h"
#include "utils/rel.h"

/*
 * 与重写操作相关的状态。对于重写设施的用户而言，这是不可见的。
 */
typedef struct RewriteStateData
{
	Relation	rs_old_rel;		/* 源堆 */
	Relation	rs_new_rel;		/* 目标堆 */
	Page		rs_buffer;		/* 当前正在构建的页面 */
	BlockNumber rs_blockno;		/* 页面将要放置的块 */
	bool		rs_buffer_valid;	/* 如果缓冲区中有任何元组则为T */
	bool		rs_logical_rewrite; /* 我们是否需要进行逻辑重写 */
	TransactionId rs_oldest_xmin;	/* 用于调用者确定元组可见性的最旧xmin */
	TransactionId rs_freeze_xid;	/* 将用作冻结截止点的Xid */
	TransactionId rs_logical_xmin;	/* 将用作逻辑重写截止点的Xid */
	MultiXactId rs_cutoff_multi;	/* 将用作多重Xact截止点的MultiXactId */
	MemoryContext rs_cxt;		/* 用于哈希表及其中的条目和元组 */
	XLogRecPtr	rs_begin_lsn;	/* 开始重写时的XLogInsertLsn */
	HTAB	   *rs_unresolved_tups; /* 不匹配的A元组 */
	HTAB	   *rs_old_new_tid_map; /* 不匹配的B元组 */
	HTAB	   *rs_logical_mappings;	/* 逻辑重映射文件 */
	uint32		rs_num_rewrite_mappings;	/* 内存映射数量 */
}			RewriteStateData;

/*
 * 哈希表的查找键是元组TID和xmin（我们必须检查
 * 两者以避免因死元组造成的错误匹配）。请注意，这个结构中可能有
 * 一些填充空间；必须将其置零以确保哈希表的正确操作。
 */
typedef struct
{
	TransactionId xmin;			/* 元组xmin */
	ItemPointerData tid;		/* 元组在旧堆中的位置 */
} TidHashKey;

/*
 * 哈希表的条目结构
 */
typedef struct
{
	TidHashKey	key;			/* B元组的预期xmin/旧位置 */
	ItemPointerData old_tid;	/* A在旧堆中的位置 */
	HeapTuple	tuple;			/* A的元组内容 */
} UnresolvedTupData;

typedef UnresolvedTupData *UnresolvedTup;

typedef struct
{
	TidHashKey	key;			/* B元组的实际xmin/旧位置 */
	ItemPointerData new_tid;	/* 在新堆中放置的位置 */
} OldToNewMappingData;

typedef OldToNewMappingData *OldToNewMapping;

/*
 * 可能需要记录逻辑重映射条目的xid的内存数据。
 */
typedef struct RewriteMappingFile
{
	TransactionId xid;			/* 可能需要查看行的xid */
	int			vfd;			/* 映射文件的fd */
	off_t		off;			/* 我们已经写入了多远 */
	uint32		num_mappings;	/* 内存映射数量 */
	dlist_head	mappings;		/* 内存映射列表 */
	char		path[MAXPGPATH];	/* 路径，用于错误消息 */
} RewriteMappingFile;

/*
 * 单个内存中的逻辑重写映射，关联
 * RewriteMappingFile->mappings。
 */
typedef struct RewriteMappingDataEntry
{
	LogicalRewriteMappingData map;	/* 旧位置和新位置之间的元组映射 */
	dlist_node	node;
} RewriteMappingDataEntry;


/* 内部函数的原型 */
static void fc_raw_heap_insert(RewriteState fc_state, HeapTuple fc_tup);

/* 内部逻辑重映射原型 */
static void fc_logical_begin_heap_rewrite(RewriteState fc_state);
static void fc_logical_rewrite_heap_tuple(RewriteState fc_state, ItemPointerData fc_old_tid, HeapTuple fc_new_tuple);
static void fc_logical_end_heap_rewrite(RewriteState fc_state);


/*
 * 开始重写一个表
 *
 * old_heap		将从中读取的旧的、锁定的堆关系元组
 * new_heap		要插入元组的新、锁定堆关系
 * oldest_xmin	调用者用于确定哪些元组是死元组的xid
 * freeze_xid	在此之前将冻结元组的xid
 * cutoff_multi	在此之前将移除多重的multixact
 *
 * 返回一个分配在当前内存上下文中的不透明RewriteState，
 * 以便在后续调用其他函数时使用。
 */
RewriteState
begin_heap_rewrite(Relation fc_old_heap, Relation fc_new_heap, TransactionId fc_oldest_xmin,
				   TransactionId fc_freeze_xid, MultiXactId fc_cutoff_multi)
{
	RewriteState fc_state;
	MemoryContext fc_rw_cxt;
	MemoryContext fc_old_cxt;
	HASHCTL		fc_hash_ctl;

	/*
	 * 为了简化清理，创建一个单独的上下文，该上下文将包含
	 * RewriteState结构本身和所有附属数据。
	 */
	fc_rw_cxt = AllocSetContextCreate(CurrentMemoryContext,
								   "Table rewrite",
								   ALLOCSET_DEFAULT_SIZES);
	fc_old_cxt = MemoryContextSwitchTo(fc_rw_cxt);

	/* 创建并填写状态结构 */
	fc_state = palloc0(sizeof(RewriteStateData));

	fc_state->rs_old_rel = fc_old_heap;
	fc_state->rs_new_rel = fc_new_heap;
	fc_state->rs_buffer = (Page) palloc(BLCKSZ);
	/* new_heap不需要为空，只需被锁定 */
	fc_state->rs_blockno = RelationGetNumberOfBlocks(fc_new_heap);
	fc_state->rs_buffer_valid = false;
	fc_state->rs_oldest_xmin = fc_oldest_xmin;
	fc_state->rs_freeze_xid = fc_freeze_xid;
	fc_state->rs_cutoff_multi = fc_cutoff_multi;
	fc_state->rs_cxt = fc_rw_cxt;

	/* 初始化用于跟踪更新链的哈希表 */
	fc_hash_ctl.keysize = sizeof(TidHashKey);
	fc_hash_ctl.entrysize = sizeof(UnresolvedTupData);
	fc_hash_ctl.hcxt = fc_state->rs_cxt;

	fc_state->rs_unresolved_tups =
		hash_create("Rewrite / Unresolved ctids",
					128,		/* 任意初始大小 */
					&fc_hash_ctl,
					HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	fc_hash_ctl.entrysize = sizeof(OldToNewMappingData);

	fc_state->rs_old_new_tid_map =
		hash_create("Rewrite / Old to new tid map",
					128,		/* 任意初始大小 */
					&fc_hash_ctl,
					HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	MemoryContextSwitchTo(fc_old_cxt);

	fc_logical_begin_heap_rewrite(fc_state);

	return fc_state;
}

/*
 * 结束重写。
 *
 * 状态和任何其他资源都被释放。
 */
void end_heap_rewrite(RewriteState fc_state)
{
	HASH_SEQ_STATUS fc_seq_status;
	UnresolvedTup fc_unresolved;

	/*
	 * 将所有剩余的元组写入 UnresolvedTups 表。如果我们还有剩余的，它们实际上应该是无效的，但我们还是谨慎些比较好。
	 */
	hash_seq_init(&fc_seq_status, fc_state->rs_unresolved_tups);

	while ((fc_unresolved = hash_seq_search(&fc_seq_status)) != NULL)
	{
		ItemPointerSetInvalid(&fc_unresolved->tuple->t_data->t_ctid);
		fc_raw_heap_insert(fc_state, fc_unresolved->tuple);
	}

	/* 写入最后一页（如果有的话） */
	if (fc_state->rs_buffer_valid)
	{
		if (RelationNeedsWAL(fc_state->rs_new_rel))
			log_newpage(&fc_state->rs_new_rel->rd_node,
						MAIN_FORKNUM,
						fc_state->rs_blockno,
						fc_state->rs_buffer,
						true);

		PageSetChecksumInplace(fc_state->rs_buffer, fc_state->rs_blockno);

		smgrextend(RelationGetSmgr(fc_state->rs_new_rel), MAIN_FORKNUM,
				   fc_state->rs_blockno, (char *) fc_state->rs_buffer, true);
	}

	/*
	 * 当我们对 rel 页进行 WAL 日志记录时，我们仍然必须对它们进行 fsync。原因与 storage.c 的 RelationCopyStorage() 中相同：我们正在写入不在共享缓冲区中的数据，因此在 rewriteheap 操作期间发生的 CHECKPOINT 将不会有我们在检查点之前写入的已 fsync 的数据。
	 */
	if (RelationNeedsWAL(fc_state->rs_new_rel))
		smgrimmedsync(RelationGetSmgr(fc_state->rs_new_rel), MAIN_FORKNUM);

	fc_logical_end_heap_rewrite(fc_state);

	/* 删除上下文会释放所有内容 */
	MemoryContextDelete(fc_state->rs_cxt);
}

/*
 * 向新 heap 添加一个元组。
 *
 * 可见性信息从原始元组中复制，除了我们将非常旧的元组“冻结”。 请注意，由于我们在 new_tuple 上进行操作，最好将其存储在临时存储中，而不是指向原始元组的指针。
 *
 * state		由 begin_heap_rewrite 返回的不透明状态
 * old_tuple	旧 heap 中的原始元组
 * new_tuple	要插入到新 heap 的新重写元组
 */
void rewrite_heap_tuple(RewriteState fc_state,
				   HeapTuple fc_old_tuple, HeapTuple fc_new_tuple)
{
	MemoryContext fc_old_cxt;
	ItemPointerData fc_old_tid;
	TidHashKey	fc_hashkey;
	bool		fc_found;
	bool		fc_free_new;

	fc_old_cxt = MemoryContextSwitchTo(fc_state->rs_cxt);

	/*
	 * 将原始元组的可见性信息复制到 new_tuple 中。
	 *
	 * XXX 我们可能之后还需要复制一些 t_infomask2 位？现在我们故意清除了 HOT 状态位。
	 */
	memcpy(&fc_new_tuple->t_data->t_choice.t_heap,
		   &fc_old_tuple->t_data->t_choice.t_heap,
		   sizeof(HeapTupleFields));

	fc_new_tuple->t_data->t_infomask &= ~HEAP_XACT_MASK;
	fc_new_tuple->t_data->t_infomask2 &= ~HEAP2_XACT_MASK;
	fc_new_tuple->t_data->t_infomask |=
		fc_old_tuple->t_data->t_infomask & HEAP_XACT_MASK;

	/*
	 * 当我们手中持有元组时，我们最好冻结任何符合条件的 xmin 或 xmax，以便将来可以节省 VACUUM 工作。
	 */
	heap_freeze_tuple(fc_new_tuple->t_data,
					  fc_state->rs_old_rel->rd_rel->relfrozenxid,
					  fc_state->rs_old_rel->rd_rel->relminmxid,
					  fc_state->rs_freeze_xid,
					  fc_state->rs_cutoff_multi);

	/*
	 * 无效的 ctid 意味着 ctid 应该指向元组本身。如果元组是更新链的一部分，我们稍后会覆盖它。
	 */
	ItemPointerSetInvalid(&fc_new_tuple->t_data->t_ctid);

	/*
	 * 如果元组已被更新，请检查旧到新的映射哈希表。
	 */
	if (!((fc_old_tuple->t_data->t_infomask & HEAP_XMAX_INVALID) ||
		  HeapTupleHeaderIsOnlyLocked(fc_old_tuple->t_data)) &&
		!HeapTupleHeaderIndicatesMovedPartitions(fc_old_tuple->t_data) &&
		!(ItemPointerEquals(&(fc_old_tuple->t_self),
							&(fc_old_tuple->t_data->t_ctid))))
	{
		OldToNewMapping fc_mapping;

		memset(&fc_hashkey, 0, sizeof(fc_hashkey));
		fc_hashkey.xmin = HeapTupleHeaderGetUpdateXid(fc_old_tuple->t_data);
		fc_hashkey.tid = fc_old_tuple->t_data->t_ctid;

		fc_mapping = (OldToNewMapping)
			hash_search(fc_state->rs_old_new_tid_map, &fc_hashkey,
						HASH_FIND, NULL);

		if (fc_mapping != NULL)
		{
			/*
			 * 我们已经复制了 t_ctid 指向的元组，因此我们可以将此元组的 ctid 设置为指向新位置，并立即插入它。
			 */
			fc_new_tuple->t_data->t_ctid = fc_mapping->new_tid;

			/* 我们不再需要映射条目 */
			hash_search(fc_state->rs_old_new_tid_map, &fc_hashkey,
						HASH_REMOVE, &fc_found);
			Assert(fc_found);
		}
		else
		{
			/*
			 * 我们还没有看到 t_ctid 指向的元组。将此元组暂存到 unresolved_tups 中，以便稍后写入。
			 */
			UnresolvedTup fc_unresolved;

			fc_unresolved = hash_search(fc_state->rs_unresolved_tups, &fc_hashkey,
									 HASH_ENTER, &fc_found);
			Assert(!fc_found);

			fc_unresolved->old_tid = fc_old_tuple->t_self;
			fc_unresolved->tuple = heap_copytuple(fc_new_tuple);

			/*
			 * 现在我们无法再做任何事情，因为我们不知道元组将被写入的位置。
			 */
			MemoryContextSwitchTo(fc_old_cxt);
			return;
		}
	}

	/*
	 * 现在我们将写入这个元组，然后检查它是否是任何新对或已知对中的 B 元组。当我们解析一个已知对时，我们将能够写入该对的 A 元组，然后我们必须检查它是否解析了其他对。因此，我们需要在这里进行循环。
	 */
	fc_old_tid = fc_old_tuple->t_self;
	fc_free_new = false;

	for (;;)
	{
		ItemPointerData fc_new_tid;

		/* 插入元组并找出它在 new_heap 中的位置 */
		fc_raw_heap_insert(fc_state, fc_new_tuple);
		fc_new_tid = fc_new_tuple->t_self;

		fc_logical_rewrite_heap_tuple(fc_state, fc_old_tid, fc_new_tuple);

		/*
		 * 如果元组是某行的更新版本，而先前版本还不至于 DEAD，那么我们要么需要解析先前版本（如果它在 rs_unresolved_tups 中待处理），要么在 rs_old_new_tid_map 中创建一条条目（以便在看到时可以解析它）。前一个元组的 xmax 将等于这个元组的 xmin，因此只有当 xmin 不早于 OldestXmin 时，它才是 RECENTLY_DEAD。
		 */
		if ((fc_new_tuple->t_data->t_infomask & HEAP_UPDATED) &&
			!TransactionIdPrecedes(HeapTupleHeaderGetXmin(fc_new_tuple->t_data),
								   fc_state->rs_oldest_xmin))
		{
			/*
			 * 好吧，这在更新对中是 B。看看我们是否见过 A。
			 */
			UnresolvedTup fc_unresolved;

			memset(&fc_hashkey, 0, sizeof(fc_hashkey));
			fc_hashkey.xmin = HeapTupleHeaderGetXmin(fc_new_tuple->t_data);
			fc_hashkey.tid = fc_old_tid;

			fc_unresolved = hash_search(fc_state->rs_unresolved_tups, &fc_hashkey,
									 HASH_FIND, NULL);

			if (fc_unresolved != NULL)
			{
				/*
				 * 我们已经看到并记住了之前的元组。现在
				 * 由于我们知道插入的元组的 t_ctid 指向的位置，
				 * 修正它的 t_ctid 并将其插入到新的堆中。
				 */
				if (fc_free_new)
					heap_freetuple(fc_new_tuple);
				fc_new_tuple = fc_unresolved->tuple;
				fc_free_new = true;
				fc_old_tid = fc_unresolved->old_tid;
				fc_new_tuple->t_data->t_ctid = fc_new_tid;

				/*
				 * 我们不再需要哈希条目，但暂时不要释放它的元组。
				 */
				hash_search(fc_state->rs_unresolved_tups, &fc_hashkey,
							HASH_REMOVE, &fc_found);
				Assert(fc_found);

				/* 循环回去将之前的元组插入链中 */
				continue;
			}
			else
			{
				/*
				 * 记住这个元组的新 tid。我们将用它在链中找到
				 * 之前的元组时设置 ctid。
				 */
				OldToNewMapping fc_mapping;

				fc_mapping = hash_search(fc_state->rs_old_new_tid_map, &fc_hashkey,
									  HASH_ENTER, &fc_found);
				Assert(!fc_found);

				fc_mapping->new_tid = fc_new_tid;
			}
		}

		/* 目前完成这个（链中的）元组 */
		if (fc_free_new)
			heap_freetuple(fc_new_tuple);
		break;
	}

	MemoryContextSwitchTo(fc_old_cxt);
}

/*
 * 在进行中的重写中注册一个死元组。死元组不会被
 * 复制到新表中，但我们仍然会记录它们，以便可以
 * 及早释放一些资源。
 *
 * 如果从 unresolved_tups 表中移除了一个元组，返回true。
 * 这表明之前被认为是“最近死亡”的元组，
 * 现在被知晓为真正死亡，且不会被写入输出。
 */
bool rewrite_heap_dead_tuple(RewriteState fc_state, HeapTuple fc_old_tuple)
{
	/*
	 * 如果我们已经在更新链中看到一个指向这个元组的早期元组，
	 * 那就忘掉那个早期元组吧。其实它也是死元组，
	 * 我们简单的 xmax < OldestXmin 测试在
	 * HeapTupleSatisfiesVacuum 中根本无法检测到它。
	 * 它发生在一个元组的 xmin 大于 xmax 时，
	 * 这听起来违反直觉，但这是完全有效的。
	 *
	 * 我们不费心试图以相反的方式检测这种情况，
	 * 当我们首先遇到死元组，然后遇到指向它的最近死元组时。
	 * 如果发生这种情况，最终我们会在 UnresolvedTups 哈希表中
	 * 有一些未匹配的条目。无论如何，这种情况都可能发生，
	 * 因为清扫可能在我们之前已经删除了链中的死元组。
	 */
	UnresolvedTup fc_unresolved;
	TidHashKey	fc_hashkey;
	bool		fc_found;

	memset(&fc_hashkey, 0, sizeof(fc_hashkey));
	fc_hashkey.xmin = HeapTupleHeaderGetXmin(fc_old_tuple->t_data);
	fc_hashkey.tid = fc_old_tuple->t_self;

	fc_unresolved = hash_search(fc_state->rs_unresolved_tups, &fc_hashkey,
							 HASH_FIND, NULL);

	if (fc_unresolved != NULL)
	{
		/* 需要释放包含的元组以及哈希表条目 */
		heap_freetuple(fc_unresolved->tuple);
		hash_search(fc_state->rs_unresolved_tups, &fc_hashkey,
					HASH_REMOVE, &fc_found);
		Assert(fc_found);
		return true;
	}

	return false;
}

/*
 * 将一个元组插入到新关系中。这必须跟踪 heap_insert
 * 及其附属功能！
 *
 * 元组的 t_self 被设置为元组的新 TID。如果元组的 t_ctid 在
 * 进入时无效，则也会被替换为新 TID（仅在插入的数据中，
 * 而不在调用者的副本中）。
 */
static void fc_raw_heap_insert(RewriteState fc_state, HeapTuple fc_tup)
{
	Page		fc_page = fc_state->rs_buffer;
	Size		fc_pageFreeSpace,
				fc_saveFreeSpace;
	Size		fc_len;
	OffsetNumber fc_newoff;
	HeapTuple	fc_heaptup;

	/*
	 * 如果新元组对于存储来说太大，或者包含已经被吐司
	 * 到其他关系中的外部属性，调用吐司器。
	 *
	 * 注意：在这一点以下，heaptup 是我们实际上打算
	 * 存储到关系中的数据；tup 是调用者的原始未吐司数据。
	 */
	if (fc_state->rs_new_rel->rd_rel->relkind == RELKIND_TOASTVALUE)
	{
		/* toast 表条目永远不应递归 toast */
		Assert(!HeapTupleHasExternal(fc_tup));
		fc_heaptup = fc_tup;
	}
	else if (HeapTupleHasExternal(fc_tup) || fc_tup->t_len > TOAST_TUPLE_THRESHOLD)
	{
		int			fc_options = HEAP_INSERT_SKIP_FSM;

		/*
		 * 在进行 VACUUM FULL / CLUSTER 的堆重写时，确保
		 * TOAST 表的数据不会被逻辑解码。主堆作为
		 * XLOG FPI 记录被 WAL 记录，这些记录不会被逻辑解码。
		 */
		fc_options |= HEAP_INSERT_NO_LOGICAL;

		fc_heaptup = heap_toast_insert_or_update(fc_state->rs_new_rel, fc_tup, NULL,
											  fc_options);
	}
	else
		fc_heaptup = fc_tup;

	fc_len = MAXALIGN(fc_heaptup->t_len); /* 保持保守 */

	/*
	 * 如果我们要因超大元组而失败，请立即处理
	 */
	if (fc_len > MaxHeapTupleSize)
		ereport(ERROR,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg("row is too big: size %zu, maximum size %zu",
						fc_len, MaxHeapTupleSize)));

	/* 根据 fillfactor 选项计算所需的额外空闲空间 */
	fc_saveFreeSpace = RelationGetTargetPageFreeSpace(fc_state->rs_new_rel,
												   HEAP_DEFAULT_FILLFACTOR);

	/* 现在我们可以检查是否已经有足够的自由空间。 */
	if (fc_state->rs_buffer_valid)
	{
		fc_pageFreeSpace = PageGetHeapFreeSpace(fc_page);

		if (fc_len + fc_saveFreeSpace > fc_pageFreeSpace)
		{
			/*
			 * 不符合条件，因此写出现有页面。它始终
			 * 包含一个元组。因此，与RelationGetBufferForTuple()不同， 
			 * 始终强制不条件地执行saveFreeSpace。
			 */

			/* XLOG 相关内容 */
			if (RelationNeedsWAL(fc_state->rs_new_rel))
				log_newpage(&fc_state->rs_new_rel->rd_node,
							MAIN_FORKNUM,
							fc_state->rs_blockno,
							fc_page,
							true);

			/*
			 * 现在写入页面。我们说skipFsync = true，因为没有必要
			 * 让smgr为此写入调度fsync；我们将在end_heap_rewrite中
			 * 自行处理。
			 */
			PageSetChecksumInplace(fc_page, fc_state->rs_blockno);

			smgrextend(RelationGetSmgr(fc_state->rs_new_rel), MAIN_FORKNUM,
					   fc_state->rs_blockno, (char *) fc_page, true);

			fc_state->rs_blockno++;
			fc_state->rs_buffer_valid = false;
		}
	}

	if (!fc_state->rs_buffer_valid)
	{
		/* 初始化一个新的空页面 */
		PageInit(fc_page, BLCKSZ, 0);
		fc_state->rs_buffer_valid = true;
	}

	/* 现在我们可以将元组插入页面中 */
	fc_newoff = PageAddItem(fc_page, (Item) fc_heaptup->t_data, fc_heaptup->t_len,
						 InvalidOffsetNumber, false, true);
	if (fc_newoff == InvalidOffsetNumber)
		elog(ERROR, "failed to add tuple");

	/* 更新调用者的t_self为实际存储位置 */
	ItemPointerSet(&(fc_tup->t_self), fc_state->rs_blockno, fc_newoff);

	/*
	 * 如果调用者没有提供有效的CTID，也将正确的位置
	 * 插入存储元组的CTID中。
	 */
	if (!ItemPointerIsValid(&fc_tup->t_data->t_ctid))
	{
		ItemId		fc_newitemid;
		HeapTupleHeader fc_onpage_tup;

		fc_newitemid = PageGetItemId(fc_page, fc_newoff);
		fc_onpage_tup = (HeapTupleHeader) PageGetItem(fc_page, fc_newitemid);

		fc_onpage_tup->t_ctid = fc_tup->t_self;
	}

	/* 如果heaptup是私有副本，则释放它。 */
	if (fc_heaptup != fc_tup)
		heap_freetuple(fc_heaptup);
}

/* ------------------------------------------------------------------------
 * 逻辑重写支持
 *
 * 在进行逻辑解码时 - 这依赖于使用目录元组的 cmin/cmax，
 * 通过 xl_heap_new_cid 记录 - 堆重写必须记录足够的信息，以便解码后端能够
 * 更新其内部映射 (relfilenode,ctid) => (cmin, cmax)，
 * 以确保重写后的堆是正确的。
 *
 * 为此，每当我们找到在 xmin 范围内的目录关系中被修改的元组时，
 * 我们就记录从旧位置到新位置的映射。
 *
 * 为了处理中止重写的情况，映射文件的文件名包含执行重写的事务的 xid，
 * 然后可以在读入之前进行检查。
 *
 * 为了提高效率，我们不会立即将每一行的每个映射溢出到磁盘，
 * 而是仅在内存中收集到几条映射后或者调用了 end_heap_rewrite() 时
 * 进行批量溢出。
 *
 * 崩溃安全性：该模块偏离了通常的 WAL 操作模式，
 * 因为它无法依赖检查点清空所有缓冲区，因此
 * 等待对缓冲区的独占锁。通常，覆盖缓冲区修改的 XLogInsert()
 * 是在对正在修改的缓冲区独占锁定的情况下执行的，以保证
 * WAL 记录和修改后的堆位于检查点的两侧。
 * 但是，由于我们记录的映射文件不在 shared_buffers 中，
 * 所以这种互锁就不起作用。
 *
 * 相反，我们简单地在执行 XLogInsert() 之前
 * 将映射文件写出到磁盘。这保证了要么 XLogInsert() 在
 * 检查点的重做指针之后插入，要么检查点（通过
 * CheckPointLogicalRewriteHeap()）已将（部分）映射文件刷新到
 * 磁盘。这使得尚未刷新到磁盘的尾部开放给
 * 损坏的问题，而通过在 xl_heap_rewrite_mapping 记录中包含
 * 当前偏移量并在重放期间将映射文件截断到该偏移量来解决。
 * 每次重写完成后，所有生成的映射文件都会同步到磁盘。
 *
 * 请注意，如果我们只关心崩溃安全性，就不必处理
 * WAL 日志记录 - 重写结束时的 fsync() 就足以保证崩溃安全性。
 * 任何尚未安全刷新到磁盘的映射都必须来自一个中止的
 * （无论是显式还是由于崩溃）事务，并且由于其名称中的
 * xid 受到 TransactionDidCommit() 检查的限制而被忽略。
 * 但我们希望支持通过物理复制来拥有备用机，
 * 既为可用性服务，也为在那里进行逻辑解码。
 * ------------------------------------------------------------------------
 */

/*
 * 如有必要，准备在重写期间记录逻辑映射。
 * 如果我们检测到不需要记录任何内容，则会阻止
 * 不同的逻辑重写函数进行任何进一步的操作。
 */
static void fc_logical_begin_heap_rewrite(RewriteState fc_state)
{
	HASHCTL		fc_hash_ctl;
	TransactionId fc_logical_xmin;

	/*
	 * 我们只需要在重写的表可以在逻辑解码期间访问时持久化这些映射，如果不能访问，我们可以跳过任何额外的工作。
	 */
	fc_state->rs_logical_rewrite =
		RelationIsAccessibleInLogicalDecoding(fc_state->rs_old_rel);

	if (!fc_state->rs_logical_rewrite)
		return;

	ProcArrayGetReplicationSlotXmin(NULL, &fc_logical_xmin);

	/*
	 * 如果没有进行的逻辑槽，我们不需要做任何事情，相关行还不能有任何重新映射。关系的锁保护我们免受竞争条件的影响。
	 */
	if (fc_logical_xmin == InvalidTransactionId)
	{
		fc_state->rs_logical_rewrite = false;
		return;
	}

	fc_state->rs_logical_xmin = fc_logical_xmin;
	fc_state->rs_begin_lsn = GetXLogInsertRecPtr();
	fc_state->rs_num_rewrite_mappings = 0;

	fc_hash_ctl.keysize = sizeof(TransactionId);
	fc_hash_ctl.entrysize = sizeof(RewriteMappingFile);
	fc_hash_ctl.hcxt = fc_state->rs_cxt;

	fc_state->rs_logical_mappings =
		hash_create("Logical rewrite mapping",
					128,		/* 任意初始大小 */
					&fc_hash_ctl,
					HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}

/*
 * 刷新所有逻辑内存映射到磁盘，但尚未进行文件同步(fsync)。
 */
static void fc_logical_heap_rewrite_flush_mappings(RewriteState fc_state)
{
	HASH_SEQ_STATUS fc_seq_status;
	RewriteMappingFile *fc_src;
	dlist_mutable_iter fc_iter;

	Assert(fc_state->rs_logical_rewrite);

	/* 没有进行的逻辑重写，无需遍历映射 */
	if (fc_state->rs_num_rewrite_mappings == 0)
		return;

	elog(DEBUG1, "flushing %u logical rewrite mapping entries",
		 fc_state->rs_num_rewrite_mappings);

	hash_seq_init(&fc_seq_status, fc_state->rs_logical_mappings);
	while ((fc_src = (RewriteMappingFile *) hash_seq_search(&fc_seq_status)) != NULL)
	{
		char	   *fc_waldata;
		char	   *fc_waldata_start;
		xl_heap_rewrite_mapping fc_xlrec;
		Oid			fc_dboid;
		uint32		fc_len;
		int			fc_written;

		/* 该文件没有任何新的映射 */
		if (fc_src->num_mappings == 0)
			continue;

		if (fc_state->rs_old_rel->rd_rel->relisshared)
			fc_dboid = InvalidOid;
		else
			fc_dboid = MyDatabaseId;

		fc_xlrec.num_mappings = fc_src->num_mappings;
		fc_xlrec.mapped_rel = RelationGetRelid(fc_state->rs_old_rel);
		fc_xlrec.mapped_xid = fc_src->xid;
		fc_xlrec.mapped_db = fc_dboid;
		fc_xlrec.offset = fc_src->off;
		fc_xlrec.start_lsn = fc_state->rs_begin_lsn;

		/* 连续写入所有映射 */
		fc_len = fc_src->num_mappings * sizeof(LogicalRewriteMappingData);
		fc_waldata_start = fc_waldata = palloc(fc_len);

		/*
		 * 收集我们需要写出的数据，但尚未修改磁盘上的数据
		 */
		dlist_foreach_modify(fc_iter, &fc_src->mappings)
		{
			RewriteMappingDataEntry *fc_pmap;

			fc_pmap = dlist_container(RewriteMappingDataEntry, node, fc_iter.cur);

			memcpy(fc_waldata, &fc_pmap->map, sizeof(fc_pmap->map));
			fc_waldata += sizeof(fc_pmap->map);

			/* 从列表中移除并释放 */
			dlist_delete(&fc_pmap->node);
			pfree(fc_pmap);

			/* 更新账簿 */
			fc_state->rs_num_rewrite_mappings--;
			fc_src->num_mappings--;
		}

		Assert(fc_src->num_mappings == 0);
		Assert(fc_waldata == fc_waldata_start + fc_len);

		/*
		 * 请注意，我们在这里偏离了常规的WAL编码实践，
		 * 请查看上述的“逻辑重写支持”注释以获取原因。
		 */
		fc_written = FileWrite(fc_src->vfd, fc_waldata_start, fc_len, fc_src->off,
							WAIT_EVENT_LOGICAL_REWRITE_WRITE);
		if (fc_written != fc_len)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not write to file \"%s\", wrote %d of %d: %m", fc_src->path,
							fc_written, fc_len)));
		fc_src->off += fc_len;

		XLogBeginInsert();
		XLogRegisterData((char *) (&fc_xlrec), sizeof(fc_xlrec));
		XLogRegisterData(fc_waldata_start, fc_len);

		/* 写入xlog记录 */
		XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_REWRITE);

		pfree(fc_waldata_start);
	}
	Assert(fc_state->rs_num_rewrite_mappings == 0);
}

/*
 * 逻辑重新映射结束部分end_heap_rewrite()。
 */
static void fc_logical_end_heap_rewrite(RewriteState fc_state)
{
	HASH_SEQ_STATUS fc_seq_status;
	RewriteMappingFile *fc_src;

	/* 完成，没有进行的逻辑重写 */
	if (!fc_state->rs_logical_rewrite)
		return;

	/* 写出剩余的内存条目 */
	if (fc_state->rs_num_rewrite_mappings > 0)
		fc_logical_heap_rewrite_flush_mappings(fc_state);

	/* 遍历我们已写入的所有映射并进行文件同步(fsync)。 */
	hash_seq_init(&fc_seq_status, fc_state->rs_logical_mappings);
	while ((fc_src = (RewriteMappingFile *) hash_seq_search(&fc_seq_status)) != NULL)
	{
		if (FileSync(fc_src->vfd, WAIT_EVENT_LOGICAL_REWRITE_SYNC) != 0)
			ereport(data_sync_elevel(ERROR),
					(errcode_for_file_access(),
					 errmsg("could not fsync file \"%s\": %m", fc_src->path)));
		FileClose(fc_src->vfd);
	}
	/* 内存上下文清理将处理其余部分 */
}

/*
 * 记录一个单一的（旧->新）映射对于'xid'。
 */
static void fc_logical_rewrite_log_mapping(RewriteState fc_state, TransactionId fc_xid,
							LogicalRewriteMappingData *fc_map)
{
	RewriteMappingFile *fc_src;
	RewriteMappingDataEntry *fc_pmap;
	Oid			fc_relid;
	bool		fc_found;

	fc_relid = RelationGetRelid(fc_state->rs_old_rel);

	/* 查找此“映射”xid的现有映射 */
	fc_src = hash_search(fc_state->rs_logical_mappings, &fc_xid,
					  HASH_ENTER, &fc_found);

	/*
	 * 我们还没有需要为这个xid映射任何内容，创建每个xid的数据结构。
	 */
	if (!fc_found)
	{
		char		fc_path[MAXPGPATH];
		Oid			fc_dboid;

		if (fc_state->rs_old_rel->rd_rel->relisshared)
			fc_dboid = InvalidOid;
		else
			fc_dboid = MyDatabaseId;

		snprintf(fc_path, MAXPGPATH,
				 "pg_logical/mappings/" LOGICAL_REWRITE_FORMAT,
				 fc_dboid, fc_relid,
				 LSN_FORMAT_ARGS(fc_state->rs_begin_lsn),
				 fc_xid, GetCurrentTransactionId());

		dlist_init(&fc_src->mappings);
		fc_src->num_mappings = 0;
		fc_src->off = 0;
		memcpy(fc_src->path, fc_path, sizeof(fc_path));
		fc_src->vfd = PathNameOpenFile(fc_path,
									O_CREAT | O_EXCL | O_WRONLY | PG_BINARY);
		if (fc_src->vfd < 0)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not create file \"%s\": %m", fc_path)));
	}

	fc_pmap = MemoryContextAlloc(fc_state->rs_cxt,
							  sizeof(RewriteMappingDataEntry));
	memcpy(&fc_pmap->map, fc_map, sizeof(LogicalRewriteMappingData));
	dlist_push_tail(&fc_src->mappings, &fc_pmap->node);
	fc_src->num_mappings++;
	fc_state->rs_num_rewrite_mappings++;

	/*
	 * 每当我们在所有映射文件中有太多内存条目时写出缓冲区。
	 */
	if (fc_state->rs_num_rewrite_mappings >= 1000 /* 任意数字 */ )
		fc_logical_heap_rewrite_flush_mappings(fc_state);
}

/*
 * 对于通过rewrite_heap_tuple()从old_tid映射到new_tuple->t_self的元组执行逻辑重新映射(如有必要)。
 */
static void fc_logical_rewrite_heap_tuple(RewriteState fc_state, ItemPointerData fc_old_tid,
						   HeapTuple fc_new_tuple)
{
	ItemPointerData fc_new_tid = fc_new_tuple->t_self;
	TransactionId fc_cutoff = fc_state->rs_logical_xmin;
	TransactionId fc_xmin;
	TransactionId fc_xmax;
	bool		fc_do_log_xmin = false;
	bool		fc_do_log_xmax = false;
	LogicalRewriteMappingData fc_map;

	/* 没有进行的逻辑重写，我们无需记录任何内容 */
	if (!fc_state->rs_logical_rewrite)
		return;

	fc_xmin = HeapTupleHeaderGetXmin(fc_new_tuple->t_data);
	/* 使用 *GetUpdateXid 正确处理多事务 */
	fc_xmax = HeapTupleHeaderGetUpdateXid(fc_new_tuple->t_data);

	/*
	 * 仅当元组最近创建时记录该映射。
	 */
	if (TransactionIdIsNormal(fc_xmin) && !TransactionIdPrecedes(fc_xmin, fc_cutoff))
		fc_do_log_xmin = true;

	if (!TransactionIdIsNormal(fc_xmax))
	{
		/*
		 * 没有设置xmax，因此不能有任何永久的，因此此检查
		 * 是足够的。
		 */
	}
	else if (HEAP_XMAX_IS_LOCKED_ONLY(fc_new_tuple->t_data->t_infomask))
	{
		/* 只是被锁定，我们不在乎 */
	}
	else if (!TransactionIdPrecedes(fc_xmax, fc_cutoff))
	{
		/* 元组最近被删除，记录 */
		fc_do_log_xmax = true;
	}

	/* 如果都不需要记录，我们完成了 */
	if (!fc_do_log_xmin && !fc_do_log_xmax)
		return;

	/* 填写映射信息 */
	fc_map.old_node = fc_state->rs_old_rel->rd_node;
	fc_map.old_tid = fc_old_tid;
	fc_map.new_node = fc_state->rs_new_rel->rd_node;
	fc_map.new_tid = fc_new_tid;

	/* ---
	 * 现在为受影响的每个xid持久化映射。如果xmin和xmax不是同一个事务，
	 * 我们需要为它们都记录，因为映射文件是按“受影响”xid来区分的。
	 * 我们在检测xmin和xmax是否实际上是同一个事务时没有花费太多努力，
	 * 我们只是检查xid是否相同，不考虑子事务。记录过多是相对
	 * 无害的，并且我们无法完全进行检查，因为在重启期间子事务
	 * 数据会被丢弃。
	 * ---
	 */
	if (fc_do_log_xmin)
		fc_logical_rewrite_log_mapping(fc_state, fc_xmin, &fc_map);
	/* 除非重复，否则单独记录xmax的映射 */
	if (fc_do_log_xmax && !TransactionIdEquals(fc_xmin, fc_xmax))
		fc_logical_rewrite_log_mapping(fc_state, fc_xmax, &fc_map);
}

/*
 * 重放XLOG_HEAP2_REWRITE记录
 */
void heap_xlog_logical_rewrite(XLogReaderState *fc_r)
{
	char		fc_path[MAXPGPATH];
	int			fc_fd;
	xl_heap_rewrite_mapping *fc_xlrec;
	uint32		fc_len;
	char	   *fc_data;

	fc_xlrec = (xl_heap_rewrite_mapping *) XLogRecGetData(fc_r);

	snprintf(fc_path, MAXPGPATH,
			 "pg_logical/mappings/" LOGICAL_REWRITE_FORMAT,
			 fc_xlrec->mapped_db, fc_xlrec->mapped_rel,
			 LSN_FORMAT_ARGS(fc_xlrec->start_lsn),
			 fc_xlrec->mapped_xid, XLogRecGetXid(fc_r));

	fc_fd = OpenTransientFile(fc_path,
						   O_CREAT | O_WRONLY | PG_BINARY);
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m", fc_path)));

	/*
	 * 截断所有数据，这些数据未被保证安全地 fsynced（通过
	 * 之前的记录或最后的检查点）。
	 */
	pgstat_report_wait_start(WAIT_EVENT_LOGICAL_REWRITE_TRUNCATE);
	if (ftruncate(fc_fd, fc_xlrec->offset) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not truncate file \"%s\" to %u: %m",
						fc_path, (uint32) fc_xlrec->offset)));
	pgstat_report_wait_end();

	fc_data = XLogRecGetData(fc_r) + sizeof(*fc_xlrec);

	fc_len = fc_xlrec->num_mappings * sizeof(LogicalRewriteMappingData);

	/* 再次写出映射文件的尾部 */
	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_LOGICAL_REWRITE_MAPPING_WRITE);
	if (pg_pwrite(fc_fd, fc_data, fc_len, fc_xlrec->offset) != fc_len)
	{
		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		if (errno == 0)
			errno = ENOSPC;
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write to file \"%s\": %m", fc_path)));
	}
	pgstat_report_wait_end();

	/*
	 * 现在 fsync 所有先前写入的数据。我们可以改善情况，仅对
	 * 文件的最后一次写入执行此操作，但所需的簿记
	 * 似乎不值得麻烦。
	 */
	pgstat_report_wait_start(WAIT_EVENT_LOGICAL_REWRITE_MAPPING_SYNC);
	if (pg_fsync(fc_fd) != 0)
		ereport(data_sync_elevel(ERROR),
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m", fc_path)));
	pgstat_report_wait_end();

	if (CloseTransientFile(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_path)));
}

/* ---
 * 执行逻辑重写映射的检查点
 *
 * 这完成两个任务：
 * 1) 根据逻辑重启 LSN 移除所有不再需要的映射
 * 2) 将所有剩余映射刷新到磁盘，以便在检查点后重放
 *	  仅需处理在检查点开始后已写出的映射部分。
 * ---
 */
void CheckPointLogicalRewriteHeap(void)
{
	XLogRecPtr	fc_cutoff;
	XLogRecPtr	fc_redo;
	DIR		   *fc_mappings_dir;
	struct dirent *fc_mapping_de;
	char		fc_path[MAXPGPATH + 20];

	/*
	 * 我们从最后的重做指针的最小值开始。没有新的解码
	 * 插槽会在此之前开始，因此这是移除的安全上限。
	 */
	fc_redo = GetRedoRecPtr();

	/* 现在检查现有插槽的重启指针 */
	fc_cutoff = ReplicationSlotsComputeLogicalRestartLSN();

	/* 不要早于重启 lsn 开始 */
	if (fc_cutoff != InvalidXLogRecPtr && fc_redo < fc_cutoff)
		fc_cutoff = fc_redo;

	fc_mappings_dir = AllocateDir("pg_logical/mappings");
	while ((fc_mapping_de = ReadDir(fc_mappings_dir, "pg_logical/mappings")) != NULL)
	{
		struct stat fc_statbuf;
		Oid			fc_dboid;
		Oid			fc_relid;
		XLogRecPtr	fc_lsn;
		TransactionId fc_rewrite_xid;
		TransactionId fc_create_xid;
		uint32		fc_hi,
					fc_lo;

		if (strcmp(fc_mapping_de->d_name, ".") == 0 ||
			strcmp(fc_mapping_de->d_name, "..") == 0)
			continue;

		snprintf(fc_path, sizeof(fc_path), "pg_logical/mappings/%s", fc_mapping_de->d_name);
		if (lstat(fc_path, &fc_statbuf) == 0 && !S_ISREG(fc_statbuf.st_mode))
			continue;

		/* 跳过不能归我们所有的文件。 */
		if (strncmp(fc_mapping_de->d_name, "map-", 4) != 0)
			continue;

		if (sscanf(fc_mapping_de->d_name, LOGICAL_REWRITE_FORMAT,
				   &fc_dboid, &fc_relid, &fc_hi, &fc_lo, &fc_rewrite_xid, &fc_create_xid) != 6)
			elog(ERROR, "could not parse filename \"%s\"", fc_mapping_de->d_name);

		fc_lsn = ((uint64) fc_hi) << 32 | fc_lo;

		if (fc_lsn < fc_cutoff || fc_cutoff == InvalidXLogRecPtr)
		{
			elog(DEBUG1, "removing logical rewrite file \"%s\"", fc_path);
			if (unlink(fc_path) < 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not remove file \"%s\": %m", fc_path)));
		}
		else
		{
			/* 在某些操作系统上，fsync 文件需要 O_RDWR */
			int			fc_fd = OpenTransientFile(fc_path, O_RDWR | PG_BINARY);

			/*
			 * 由于并发性，该文件不能消失，因为这个函数
			 * 是唯一一个移除逻辑映射的，并且一次只能
			 * 进行一个检查点。
			 */
			if (fc_fd < 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not open file \"%s\": %m", fc_path)));

			/*
			 * 我们可以尝试避免 fsync 未更改或仅在检查点
			 * 开始后创建的文件，但目前认为不值得付出努力。
			 */
			pgstat_report_wait_start(WAIT_EVENT_LOGICAL_REWRITE_CHECKPOINT_SYNC);
			if (pg_fsync(fc_fd) != 0)
				ereport(data_sync_elevel(ERROR),
						(errcode_for_file_access(),
						 errmsg("could not fsync file \"%s\": %m", fc_path)));
			pgstat_report_wait_end();

			if (CloseTransientFile(fc_fd) != 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not close file \"%s\": %m", fc_path)));
		}
	}
	FreeDir(fc_mappings_dir);

	/* 将目录项持久化到磁盘 */
	fsync_fname("pg_logical/mappings", true);
}
